
當 LLM 可以快速協助開發API 後,接著又想打它的主意:幫忙AI化系統。早期是逐步將繁雜的系統"API化",但現在可能要朝系統"AI化"發展了。
這邊系統AI化最直覺的方法,就是讓模型辨認,使用者下了什麼樣的需求,去協助幫忙溝通與執行系統。
而能夠讓LLM隨時替換模型,又能溝通系統,MCP似乎可以在這個時間點辦到。MCP可以到https://modelcontextprotocol.io/introduction 查看,有將做法與套件公佈出來。
這邊預想實行以下構想:
MCP中可以將這些服務註冊到工具集合中。
但要如何實作出隨時抽換服務? 可以透過外部組件與讀檔註冊方式實行,概念類似抽換類別模組不重啟服務 與 外部擴充功能研究範例。也就是讀設定檔來逐一註冊組件內的工具。
先準備二個 LLM 助 手,這邊是使用 python 撰寫的 FastAPI 。
第一個助手是幫使用者辨識執行已註冊的系統溝通工具:
from google import genai
from google.genai import types
from io import BytesIO
@app.post("/CallLLM/")
async def CallLLM(data: Item = Body(...,media_type = "application/json")):
    client = genai.Client(api_key="AIhbiexKpggpWVmG8Zjs5QdtnQx8rpwhSac7X2o")
    MODEL_ID = "gemini-2.0-flash"
    user_req =  data.msg  
    proj_name = "DigitalDevProj"
    system_prompt = f"成為一個專業的程式開發者,且精通json格式。 \
        我需要你的協助產生json字串, 只需要依照使用者需求內容來產生json字串。其它以外的資訊請排除掉。"
    prompt = f' 使用者需求: {user_req}  。 ' + r'目前提供現有json物件陣列: \
    [ \
    {"title":"Add","description":"加上二個數字, 對應a是第一個數字, b是第二個數字。","type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}},"required":["a","b"]} ,   \
    {"title":"substract","description":"substract two numbers,parameter a is first integer, than b is second integer。","type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}},"required":["a","b"]}, \
    {"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["a","b"] }  \
    ] \
    ,請由使用者需求,分析對應至現有json陣列中的哪一個物件,關鍵對應是依照其中description的值。回傳找出對應的json物件,此物件包含成員為 title、description、type、properties、required,並將此物件的properties中的成員之值替換成使用者需求所給的值。 \
    如果沒有對應到,則回傳現有json物件陣列中第一個物件,並將並將所有成員的值變成空字串。  '
    response = client.models.generate_content(
        model=MODEL_ID,
        contents=[prompt],
        config=types.GenerateContentConfig(
            system_instruction=system_prompt
        )
    )
    return response.text
注意到提示給 LLM 的敘述中,有"提供現有json物件陣列",這個是代表"掃出"所註冊的執行工具所"組出"的,讓LLM可以從中(限制只能從中)找出對應的執行工具,這邊為了展示方便直接兜出來放到API中展示,之後實作MCP會看到。實際上可以在MCP服務起來後,掃註冊的所有工具服務,存到DB或其它地方,再讓FastAPI底層去讀出來讓LLM模型知道。其組成結構欄位解析如下:
//註冊工具物件類別
public class ToolDescription
{
    //工具名稱
    public string title { get; set; }
    
    //工具描述
    public string description { get; set; }
    
    //工具型態
    public string type { get; set; }
    
    //工具使用參數
    public Dictionary<string,object> properties { get; set; }
}
第二個助手是當執行發生問題後,跟使用者說明清楚要執行的目標有需要什麼內容,使用者可以較清楚知道缺了什麼。類似客服:
from google import genai
from google.genai import types
from io import BytesIO
@app.post("/CallLLM_CustomerService/")
async def CallLLM_CustomerService(data: Item = Body(...,media_type = "application/json")):
    client = genai.Client(api_key="AIhbiexKpggpWVmG8Zjs5QdtnQx8rpwhSac7X2o")
    MODEL_ID = "gemini-2.0-flash"
    
    system_prompt = f"成為一個專業的程式開發者,且精通json格式。 \
          我需要你的協助和人類溝通,說明json的內容。其它以外的資訊請排除掉。"
    prompt =  r'目前提供 json 物件陣列: \
    [ \
    {"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["ItemName","ItemPrice","Applicants","InvoiceDate","dep"] }  \
    ] \
    ,請以客服的口吻,和人類描述此json主要功能與需要的參數,參數的key值不要直接描述出來,且開頭請以這段話開始: 因為執行出了一點問題,未完成任務,這邊跟您說明此功能的資訊。 '
    response = client.models.generate_content(
        model=MODEL_ID,
        contents=[prompt],
        config=types.GenerateContentConfig(
            system_instruction=system_prompt
        )
    )
    return response.text
注意到這邊也為了展示方便,把工具物件json直接卡進去API函式中,實際上是傳進去在餵給LLM,一樣是展示清楚用。
即以下這json物件是剛剛判斷取得的工具描述內容,再餵進去客服API的物件內容:
{"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["ItemName","ItemPrice","Applicants","InvoiceDate","dep"] }  
準備好LMM後,開始實行MCP。
這邊使用微軟開發的MCP套件,語言是C#。
第一步是建立 MCP Server(當然之後可以建立多個不同用途的 MCP Server!)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Reflection;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly()
    .WithToolsFromAssembly(
               Assembly.LoadFile(@"D:\NetMCP\DLL_Extension\DLL_Extension-X\DLL_Extension\bin\Debug\DLL_Extension.dll")
            ); 
await builder.Build().RunAsync();
/// <summary>
/// 測試開通用途-執行加法-寫在建立MCP Server組件內
/// </summary>
[McpServerToolType]
public  static partial class CalculatorTool
{
    [McpServerTool, Description("add two number, parameter a is first integer, than b is second integer。") ]
    public static string Add(int a, int b) => $"add {a + b}";
}
注意其中的這段:
.WithToolsFromAssembly(
               Assembly.LoadFile(@"D:\NetMCP\DLL_Extension\DLL_Extension-X\DLL_Extension\bin\Debug\DLL_Extension.dll")
            )
有看到這段讀取嗎? 這就是給機會去讀取外部組件,達成外部設定透過讀取組件來註冊,可以改成讀設定檔取出路徑逐一讀取註冊。如果可以透過設定檔如json檔案,之後上版註冊就相對快與方便一些。
接著此展示於外部組件註冊了二個服務,一個是測試是否開通的功能,一個是正式要執行的服務 "發票申請表單填寫-起流程單"。
外部組件註冊服務工具:
using ModelContextProtocol.Server;
using System.ComponentModel;
/// <summary>
/// 測試開通用途-執行減法
/// </summary>
[McpServerToolType]
public static class CalculatorTool_EX
{
    [McpServerTool, Description("substract two numbers。parameter a is first integer, than b is second integer。")]
    public static string substract(int a, int b) => $"substract {a - b}"; 
}
/// <summary>
/// 發票申請表單填寫-起流程單
/// </summary>
[McpServerToolType]
public static class InvoiceModule_EX
{
    [McpServerTool, Description(@"發票申請,參數說明: (1) ItemName為商品名稱,型態為字串。 
                                                     (2) ItemPrice為商品價格,型態為整數。
                                                     (3) Applicants為申請者,型態為字串。
                                                     (4) InvoiceDate為發票開立日期,型態為字串。
                                                     (5) dep為申請單位,型態為字串。")]
    public static string InvoceApplicate(string ItemName, int ItemPrice, string Applicants, string InvoiceDate,string dep) 
                                        => Invoce.ExecApplicate( ItemName,  ItemPrice, Applicants, InvoiceDate); 
}
建好 MCP Server 後,繼續建立接收使用者訊息並讓 LLM 和 MCP 溝通的 Client 部分:
MCP Client
using ModelContextProtocol.Client;
using Newtonsoft.Json;
using ModelContextProtocol.Protocol;
var clientTransport = new StdioClientTransport(new()
{
    Name = "ConnetServer",
    Command = @"dotnet",
    Arguments = ["run", "--project", "D:\\NetMCP\\ConsoleMCP_Server\\ConsoleMCP.csproj"],
});
var userMessage = string.Empty; //準備接收的訊息會指派到這
string retResponse = string.Empty;
string apiUrl = "http://localhost:5005/CallLLM/";  //FastAPI:第一個助手幫使用者辨識執行已註冊的系統溝通工具
using (var client = new HttpClient())
{
    var jsonContent = "{ \"msg\" : \"" + userMessage + "\" }";  //JsonConvert.SerializeObject(data);
    var stringContent = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
    // 發出POST請求
    var response = await client.PostAsync(apiUrl, stringContent);
    if (response.IsSuccessStatusCode)
    {
        // 取得內容
        string responseContent = await response.Content.ReadAsStringAsync();
        Console.WriteLine(responseContent);
        responseContent = responseContent.Replace("\"```json", "").Replace("```\\n\"", "").Replace("\\n", "").Replace("\\", "");
        try
        {
            ToolDescription ret = JsonConvert.DeserializeObject<ToolDescription>(responseContent);
            if (ret != null && ret.title != null && ret.properties != null)
            {
                var result = ((CallToolResponse)await mcpClient.CallToolAsync(
                        ret.title,
                        ret.properties,
                        cancellationToken: CancellationToken.None
                    )
                  );
                Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.Message.ToString()); 
            //使用者看不懂錯誤訊息,要呼叫第二個助手客服工程師幫忙解析工具服務
        }   
    }
    else
    {
        // 處理錯誤情況
        Console.WriteLine($"Error: {response.StatusCode}");
    }
而將已註冊的服務掃出來,準備要讓 LLM 知道的訊息,可以利用以下來組出轉換,再存到DB或其他檔案中供FastAPI那邊讀取,這邊不實作:
public class ToolDescription
{
    public string title { get; set; }
    public string description { get; set; }
    public string type { get; set; }
    public Dictionary<string,object> properties { get; set; }
}
async Task<List<ToolDescription>> GetMcpTools()
{
    Console.WriteLine("Listing tools");
    var tools = await mcpClient.ListToolsAsync();
    List<ToolDefinition> toolDefinitions = new List<ToolDefinition>();
    foreach (var tool in tools)
    {
        Console.WriteLine($"Connected to server with tools: {tool.Name}");
        Console.WriteLine($"Tool description: {tool.Description}");
        Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
        toolDefinitions.Add(new toolDefinitions(){
                                 title = tool.Name, 
                                 description = tool.description,
                                 properties=ConverToDict(tool.JsonSchema)
                            });
    }
    return toolDefinitions;
}
開始Demo訊息:
(1)訊息 userMessage = "2 減 3"
這是做開通測試的功能。有找到已註冊的服務工具並實行此工具"減法":
(2)訊息 userMessage = "幫我送發票申請,商品名稱為手機,價格36000,申請者為喵喵有限公司,發票日期為2025-05-08,申請單位為開發部"
這是找到已註冊的服務工具"發票申請"並執行送出發票申請單,起流程。
(3)訊息 userMessage = "幫我送請假單,公假,日期是今天,時間下午13:00到17:00"
因為沒有註冊請假申請表單送出的服務,自然就是找不到的狀況。
(4)訊息 userMessage = "幫我送發 票申請,商 品 名稱為手機,申請者為喵喵 有 限,發 票日期為2025-05-08,申請單位為開 發 部"
忘了輸入價 格,不能順利申請。因為價 格為必要欄位。
當這個狀況,有找出註冊的服務,但使用者給的資訊不足,就需要將此服務需要的內容"轉述"給使用者知道。
所以要呼叫第二個助手客服工程師幫忙解析工具服務。
根據此狀況執行設定好的客服助手 LLM 後,會回傳:
因為執行出了一點問題,未完成任務,這邊跟您說明此功能的資訊。
這個功能是關於「財會發票申請」。您需要提供一些資訊來完成申請,這些資訊包括:
*   **商品名稱**:您要申請發票的品名稱。
*   **商品價格**:這個商品的價格是多少。
*   **申請者**:是誰提出這個發票申請。
*   **發票開立日期**:發票是什麼時候開立的。
*   **申請單位**:哪個單位提出這個申請。
請確保您提供了以上所有資訊,才能順利完成發票申請。
藉由上述簡單的展示,未來可以將這MCP模組化、套件化或元件化,讓它更彈性的重複利用,只需要專注在教更多模型助手協助,讓功能更加完整。
另外,使用者訊息歷程是另外一個問題,怎麼去標註是連續同一個任務,還是穿插的任務,這部分需要再規劃,或替訊息標註flag,或設置有效指令連續範圍,或再請另一個"LLM助手"來幫忙整理記憶,都需要規劃與測試。
資安與權限部分,是展示未提到的,這部分也要額外注意。